#include <tinyalsa/asoundlib.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>

#include "b_ring_buffer.h"

#undef TAG
#define TAG "BALP"
#if 0      // def 0__ANDROID__
#include <android/log.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#else
#define LOGI(str, ...) do{printf("BALP ");printf(str,##__VA_ARGS__);printf("\n");}while(0)
#define LOGD(str, ...) do{printf("BALP ");printf(str,##__VA_ARGS__);printf("\n");}while(0)
#define LOGW(str, ...) do{printf("BALP ");printf(str,##__VA_ARGS__);printf("\n");}while(0)
#define LOGE(str, ...) do{printf("BALP ");printf(str,##__VA_ARGS__);printf("\n");}while(0)
#endif

#define DEFAULT_CARD_ID (0)
#define DEFAULT_PLAYBACK_CARD_ID (0)
#define DEFAULT_CAPTURE_CARD_ID (0)
#define DEFAULT_CAPTURE_PERIOD_SIZE (1440)
#define DEFAULT_PLAYBACK_PERIOD_SIZE (768)
#define DEFAULT_PLAYBACK_PERIOD_COUNT (4)
#define DEFAULT_CAPTURE_PERIOD_COUNT (2)
#define DEFAULT_CHANNELS_STEREO (2)
#define DEFAULT_SAMPLE_RATE (48000)
#define TINYHOSTLESS_DEVICE_UNDEFINED (255)

#define DEFAULT_RING_FUBUFFER_FRAMES (2880 * DEFAULT_CAPTURE_PERIOD_COUNT * 8)

static unsigned int card = 0;
static unsigned int p_device = DEFAULT_PLAYBACK_CARD_ID;
static unsigned int c_device = DEFAULT_CAPTURE_CARD_ID;
static unsigned int capture_period_size = DEFAULT_CAPTURE_PERIOD_SIZE;
static unsigned int sink_period_size = DEFAULT_PLAYBACK_PERIOD_SIZE;
static unsigned int sink_period_count = DEFAULT_PLAYBACK_PERIOD_COUNT;
static unsigned int capture_period_count = DEFAULT_CAPTURE_PERIOD_COUNT;
static unsigned int number_bits = 16;
static unsigned int num_channels = DEFAULT_CHANNELS_STEREO;
static unsigned int sample_rate = DEFAULT_SAMPLE_RATE;
static unsigned int play_cap_time = 0;
static struct timespec expire_time_spec;
static unsigned int do_loopback = 1;
static int sink_min_channels = 12;
struct pcm *pcm_cap = NULL, *pcm_play = NULL;
static int total_cap_size, total_sink_size;

static int close_h = 0;

static int is_expire(){
    if (play_cap_time > 0) {
        struct timespec now;
        clock_gettime(CLOCK_MONOTONIC, &now);
        if (now.tv_sec > expire_time_spec.tv_sec
              || (now.tv_sec == expire_time_spec.tv_sec && now.tv_nsec >= expire_time_spec.tv_nsec)) {
            pid_t tid = getpid(); // Get the main thread ID
            LOGW("timeout %d Expires. Terminate Process %d", play_cap_time, tid);
            kill(tid, SIGTERM);
            return 1;
        }
    }
    return 0;
}

static int merge_data_to_channel(char* out_buffer, const char* in_buffer, int src_size,
                                   int src_channels, int dest_channels, int bit_width) {
    int i = 0;
    int copy_channels = src_channels;
    if (src_channels > dest_channels) copy_channels = dest_channels;
    int loop_count = src_size / bit_width / src_channels;

    for(; i < loop_count; i ++) {
        memcpy(out_buffer + i * dest_channels * bit_width,
               in_buffer + i * bit_width * src_channels,
               bit_width * copy_channels);
    }
    return src_size * dest_channels / src_channels;
}

static int check_param(struct pcm_params *params, unsigned int param,
                       unsigned int value, char *param_name, char *param_unit) {
    unsigned int min;
    unsigned int max;
    int is_within_bounds = 1;

    min = pcm_params_get_min(params, param);
    if (value < min) {
        if(strstr("Sample", param_name)) {
            sink_min_channels = min;
            LOGW("[%d] %s is %u%s, try to satisfy %u%s\n",  gettid(), param_name, value, param_unit, min, param_unit);
            return 1;
        }
        LOGE("[%d] %s is %u%s, device only supports >= %u%s\n",  gettid(), param_name, value,
                param_unit, min, param_unit);
        is_within_bounds = 0;
    }

    max = pcm_params_get_max(params, param);
    if (value > max) {
        LOGE("%s is %u%s, device only supports <= %u%s\n", param_name, value,
                param_unit, max, param_unit);
        is_within_bounds = 0;
    }

    return is_within_bounds;
}

static int check_params(unsigned int card, unsigned int device, unsigned int direction,
                        const struct pcm_config *config) {
    struct pcm_params *params;
    int can_play;
    int bits;

    params = pcm_params_get(card, device, direction);
    if (params == NULL) {
        LOGE("[%d] Unable to open PCM %s device %u.", gettid(),
                direction == PCM_OUT ? "sink" : "capture", device);
        return 0;
    }

    switch (config->format) {
    case PCM_FORMAT_S32_LE:
        bits = 32;
        break;
    case PCM_FORMAT_S24_3LE:
        bits = 24;
        break;
    case PCM_FORMAT_S16_LE:
        bits = 16;
        break;
    default:
        LOGE("[%d] Invalid format: %u", gettid(), config->format);
        return 0;
    }

    can_play = check_param(params, PCM_PARAM_RATE, config->rate, "Sample rate", "Hz");
    can_play &= check_param(params, PCM_PARAM_CHANNELS, config->channels, "Sample", " channels");
    can_play &= check_param(params, PCM_PARAM_SAMPLE_BITS, bits, "Bitwidth", " bits");
    can_play &= check_param(params, PCM_PARAM_PERIOD_SIZE, config->period_size, "Period size", " frames");
    can_play &= check_param(params, PCM_PARAM_PERIODS, config->period_count, "Period count", " periods");

    pcm_params_free(params);

    return can_play;
}

static void b_signal_handler(int sig) {
    //signal(sig, SIG_IGN);
    switch (sig) {
    case SIGINT:
    case SIGTERM:
    case SIGHUP:
    case SIGPIPE:
        goto ___exit_hdr;
    default:
        return;
    }

___exit_hdr:
    LOGI("Close pcm devices for signal %d", sig);
    close_h = 1;
    if (pcm_cap != NULL) {
        pcm_close(pcm_cap);
        pcm_cap = NULL;
    }
    if (pcm_play != NULL) {
        pcm_close(pcm_play);
        pcm_play = NULL;
    }
    exit(0);
}

void* capture_thread(void* arg) {
    struct pcm_config config;
    unsigned int count =0;
    char *buffer = NULL;
    int size = 0;
    int rc = 0;
    int retry = 0;
    ringbuffer_t *rb = (ringbuffer_t*)arg;

    memset(&config, 0, sizeof(config));
    config.channels = num_channels;
    config.rate = sample_rate;
    config.period_size = capture_period_size;
    config.period_count = capture_period_count;
    if (number_bits == 32)
        config.format = PCM_FORMAT_S32_LE;
    else if (number_bits == 24)
        config.format = PCM_FORMAT_S24_3LE;
    else if (number_bits == 16)
        config.format = PCM_FORMAT_S16_LE;
    config.start_threshold = 0;
    config.stop_threshold = 0;
    config.avail_min = 0;

    if (c_device < TINYHOSTLESS_DEVICE_UNDEFINED ) {
        if (!check_params(card, c_device, PCM_IN, &config)) {
            rc = EINVAL;
            goto ___exit_cap;
        }
        pcm_cap = pcm_open(card, c_device, PCM_IN, &config);
        if (!pcm_cap || !pcm_is_ready(pcm_cap)) {
            LOGE("Unable to open PCM capture device %u (%s)",
                    c_device, pcm_get_error(pcm_cap));
            rc = errno;
            goto ___exit_cap;
        }
    }

    if (do_loopback) {
        size = pcm_frames_to_bytes(pcm_cap, pcm_get_buffer_size(pcm_cap));
        buffer = malloc(size);
        if (!buffer) {
            LOGE("Unable to allocate %d bytes", size);
            pcm_close(pcm_cap);
            pcm_cap = NULL;
            rc = ENOMEM;
            goto ___exit_cap;
        }
    }

    if (pcm_cap != NULL) pcm_start(pcm_cap);

    do  {
        if (do_loopback) {
            if (pcm_read(pcm_cap, buffer, size)) {
                LOGE("Unable to read %d bytes from PCM capture device", size);
                rc = errno;
                break;
            }
            int valid_size = ringbuffer_available_to_write(rb);
            if(valid_size < size) {
                int delay = 5 << retry;
                //LOGW("Not enough space in buffer. Remain:%d Wait %dms", valid_size, delay);
                usleep(delay * 1000);
                if (retry < 3) {
                    retry += 1;
                }
                continue;
            }
            retry = 0;
            ringbuffer_write(rb, buffer, size);
            total_cap_size += size;
        } else {
            usleep(100000);
        }
        if(is_expire()) break;
    } while(!close_h);

___exit_cap:
    if (buffer) free(buffer);
    LOGD("capture_thread %d exit. kill %d",gettid(), getpid());
    kill(getpid(), SIGTERM);
    return (void*)(long)rc;
}

void* sink_thread(void* arg) {
    struct pcm_config config;
    unsigned int count =0;
    char *pcm_sink_buffer = NULL;
    int sink_size = 0;
    int rc = 0;
    int retry = 0;

    struct pcm_params *params;
    uint32_t sink_min_channels = 0;
    int rb_read_buffer_size = num_channels * sink_period_size * sink_period_count * number_bits / 8;
    char* rb_read_buffer = NULL;

    ringbuffer_t *rb = (ringbuffer_t*)arg;

    memset(&config, 0, sizeof(config));
    config.rate = sample_rate;
    config.period_size = sink_period_size;
    config.period_count = sink_period_count;
    if (number_bits == 32)
        config.format = PCM_FORMAT_S32_LE;
    else if (number_bits == 24)
        config.format = PCM_FORMAT_S24_3LE;
    else if (number_bits == 16)
        config.format = PCM_FORMAT_S16_LE;
    config.start_threshold = 0;
    config.stop_threshold = 0;
    config.avail_min = 0;

    params = pcm_params_get(card, p_device, PCM_OUT);
    sink_min_channels = pcm_params_get_min(params, PCM_PARAM_CHANNELS);
    if (num_channels < sink_min_channels) {
        LOGW("Convert config.channels to %d instead of %d", sink_min_channels, num_channels);
        config.channels = sink_min_channels;
    } else {
        config.channels = num_channels;
    }

    if(p_device < TINYHOSTLESS_DEVICE_UNDEFINED )  {
        if (!check_params(card, p_device, PCM_OUT, &config)) {
            rc = EINVAL;
            goto ___exit_sink;
        }
        pcm_play = pcm_open(card, p_device, PCM_OUT, &config);
        if (!pcm_play || !pcm_is_ready(pcm_play)) {
            LOGE("Unable to open PCM sink device %u (%s)",
                    p_device, pcm_get_error(pcm_play));
            rc = errno;
            goto ___exit_sink;
        }
    }

    if (do_loopback) {
        sink_size = pcm_frames_to_bytes(pcm_play, pcm_get_buffer_size(pcm_play));
        pcm_sink_buffer = malloc(sink_size);
        rb_read_buffer = (char*)malloc(rb_read_buffer_size);
        if (!pcm_sink_buffer || !rb_read_buffer) {
            LOGE("Unable to allocate %d bytes", sink_size);
            pcm_close(pcm_play);
            pcm_play = NULL;
            rc = ENOMEM;
            goto ___exit_sink;
        }
    }

    if (pcm_play != NULL) pcm_start(pcm_play);

    do  {
        if (do_loopback) {
            int valid_size = ringbuffer_available_to_read(rb);
            if(valid_size < rb_read_buffer_size) {
                int delay = 5 << retry;
                //LOGW("Just %d bytes in buffer, Expect %d bytes. Wait %dms", valid_size, rb_read_buffer_size, delay);
                usleep(delay * 1000);
                if (retry < 3) {
                    retry += 1;
                }
                continue;
            }
            size_t bytes_read = ringbuffer_read(rb, rb_read_buffer, rb_read_buffer_size);
            int write_pcm_size = 
            merge_data_to_channel(pcm_sink_buffer, rb_read_buffer, bytes_read,
                                  num_channels, sink_min_channels, number_bits);
            total_sink_size += write_pcm_size;
            retry = 0;
            if (pcm_write(pcm_play, pcm_sink_buffer, write_pcm_size)) {
                LOGW("Unable to write to PCM sink device %u (%s)",
                        p_device, pcm_get_error(pcm_play));
                break;
            }
        } else {
            usleep(100000);
        }
        if(is_expire()) break;
    } while(!close_h);

___exit_sink:
    if (pcm_sink_buffer)
        free(pcm_sink_buffer);
    if (rb_read_buffer)
        free(rb_read_buffer);
    LOGD("sink_thread %d exit. kill %d",gettid(), getpid());
    kill(getpid(), SIGTERM);
    return (void*)(long)rc;
}

//  balp -D 0 -P 0 -C 0 -pp 768 -cp 480 -pn 4 -cn 2 -ch 2 -l
int main(int argc, char **argv) {
    void* status;
    pthread_t capture_tid;
    pthread_t sink_tid;

    if (argc < 2) {
        LOGE("Usage: %s [-D card] [-P sink_device]\n"
            " [-C capture_device] [-pp sink_period_size] [-cp capture_period_size]\n"
            " [-pn sink_period_count] [-cn capture_period_count]\n"
            " [-ch num_channels] [-b number_bits] [-r sample_rate] [-l]\n"
            " [-T sink/capture time]\n\n"
            "Used to enable 'hostless' mode for audio devices with a DSP back-end.\n"
            "Alternatively, specify '-l' for loopback mode: this program will read\n"
            "from the capture device and write to the sink device.\n",
            argv[0]);
        return 1;
    }
    int rb_size = (number_bits / 8) * num_channels * DEFAULT_RING_FUBUFFER_FRAMES;
    ringbuffer_t *rb = ringbuffer_init(rb_size);
    if (rb == NULL) return -1;
    LOGI("Process %s[%d] enter, ring_buffer_size:%d", argv[0], getpid(), rb_size);

   /* parse command line arguments */
    argv += 1;
    while (*argv) {
        if (strcmp(*argv, "-D") == 0) {
            argv++;
            if (*argv)
                card = atoi(*argv);
        }
        if (strcmp(*argv, "-P") == 0) {
            argv++;
            if (*argv)
                p_device = atoi(*argv);
        }
        if (strcmp(*argv, "-C") == 0) {
            argv++;
            if (*argv)
                c_device = atoi(*argv);
        }
        if (strcmp(*argv, "-pp") == 0) {
            argv++;
            if (*argv)
                sink_period_size = atoi(*argv);
        }
        if (strcmp(*argv, "-cp") == 0) {
            argv++;
            if (*argv)
                capture_period_size = atoi(*argv);
        }
        if (strcmp(*argv, "-pn") == 0) {
            argv++;
            if (*argv)
                sink_period_count = atoi(*argv);
        }
        if (strcmp(*argv, "-cn") == 0) {
            argv++;
            if (*argv)
                capture_period_count = atoi(*argv);
        }
        if (strcmp(*argv, "-ch") == 0) {
            argv++;
            if (*argv)
                num_channels = atoi(*argv);
        }
        if (strcmp(*argv, "-b") == 0) {
            argv++;
            if (*argv)
                number_bits = atoi(*argv);
        }
        if (strcmp(*argv, "-r") == 0) {
            argv++;
            if (*argv)
                sample_rate = atoi(*argv);
        }
        if (strcmp(*argv, "-T") == 0) {
            argv++;
            if (*argv)
                play_cap_time = atoi(*argv);
        }
        if (strcmp(*argv, "-l") == 0) {
            do_loopback = 1;
        }
        if (*argv) argv++;
    }

    if (do_loopback) {
        LOGI("Loopback %ds %d:%d[%d*%d] -> %d:%d[%d*%d] ch:%d rate:%d.", play_cap_time,
            card, c_device, capture_period_size, capture_period_count,
            card, p_device, sink_period_size, sink_period_count,
            num_channels, sample_rate);
    }

    clock_gettime(CLOCK_MONOTONIC, &expire_time_spec);
    expire_time_spec.tv_sec += play_cap_time;

    if (pthread_create(&capture_tid, NULL, capture_thread, (void*)rb)) goto ___exit_main;
    if (pthread_create(&sink_tid, NULL, sink_thread, (void*)rb)) goto ___exit_main;

    /* catch ctrl-c to shutdown cleanly */
    signal(SIGINT, b_signal_handler);
    signal(SIGHUP, b_signal_handler);
    signal(SIGTERM, b_signal_handler);
    signal(SIGPIPE, b_signal_handler);

    char command[100];
    while (1) {
        scanf("%s", command);
        if((strcmp(command, "S") == 0) || (strcmp(command, "s") == 0)) {
            LOGI(" *** statistic *** \n"
            "  total_capture_size:%d\n"
            "  total_sink_size:%d\n"
            , total_cap_size, total_sink_size);
        }
        if((strcmp(command, "quit") == 0) || (strcmp(command, "q") == 0)) {
            pid_t tid = getpid(); // Get the main thread ID
            kill(tid, SIGTERM);
        }
    }

___exit_main:
    ringbuffer_free(rb);
    return 0;
}